/**
 * 
 */
package net.slique.sf5.model.toys.sine;

import java.text.DecimalFormat;
import java.text.Format;
import java.util.ArrayList;
import java.util.List;

import net.slique.sf5.model.silverware.ComputationalModel;
import net.slique.sf5.model.silverware.ModelInformation;
import net.slique.sf5.model.silverware.ModelMeta;
import net.slique.sf5.model.silverware.OkPredicateResponse;
import net.slique.sf5.model.silverware.Solution;
import net.slique.sf5.model.silverware.SolutionResponse;

import org.apache.commons.lang.StringUtils;

/**
 * @author Vikrum
 *
 */
public class SineWaveModel implements ComputationalModel {
	
	// meta
	private double x0 = -2.0 * Math.PI;
	private double x1 =  2.0 * Math.PI;
	private int steps = 100;
	
	// parameters
	private double amplitude = 1;
	private double frequency = 1;
	private double phase = 0; 
	
	// constants
	// none for this model
	
	// local state
	private List<Double> dependent = new ArrayList<Double>();
	private List<Double> independent = new ArrayList<Double>();
	private boolean trunkcatedPlot = false;
	
	
	/**
	 * @param x0
	 * @param x1
	 * @param steps
	 */
	public SineWaveModel() {
		super();
	}

	/* (non-Javadoc)
	 * @see net.slique.sf5.model.silverware.ComputationalModel#postSolve()
	 */
	public OkPredicateResponse postSolve() {
		return new OkPredicateResponse().setOk(true).setMessage(trunkcatedPlot ? "Google Chart API limit hit; plot truncated." : null);
	}

	/* (non-Javadoc)
	 * @see net.slique.sf5.model.silverware.ComputationalModel#preSolve()
	 */
	public OkPredicateResponse preSolve() {
		return new OkPredicateResponse().setOk(true);//.setMessage("Pre-solve: OK");
	}

	/* (non-Javadoc)
	 * @see net.slique.sf5.model.silverware.ComputationalModel#solve()
	 */
	public SolutionResponse solve() {
		SolutionResponse solutionResponse = new SolutionResponse();
		Solution solution = new Solution();
		solutionResponse.getSolutions().add(solution);
		double delta = (x1 - x0) / (1.0 * steps);
		for(int i = 0; i < steps; i++) {
			double x = x0 + (delta * i);
			double y = getAmplitude() * Math.sin((getFrequency() * x) + getPhase());
			dependent.add(y);
			independent.add(x);

		}
		solution.setData(makeData());
		solution.setPlot(makePlot());
		return solutionResponse;
	}

	private String makeData() {
		StringBuilder data = new StringBuilder();		
		data.append("x\ty\n");
		for(int i = 0; i < dependent.size(); i++) {
			data.append(independent.get(i)).append("\t").append(dependent.get(i)).append("\n");
		}
		return data.toString();
	}

	private String makePlot() {
		String plotUrl = new StringBuilder("http://chart.apis.google.com/chart?cht=lxy&chco=8FB9B7&chs=555x333&chd=t:").append(getPlotFragment()).toString();
		if(plotUrl.length() > 2000) {
			trunkcatedPlot = true;
			plotUrl = plotUrl.substring(0, 2000);
		}
		return plotUrl;
	}

	public String getPlotFragment() {
		double minimum = Double.MAX_VALUE;
		double maximum = Double.MIN_VALUE;
		
		// find min & max
		for (Double y : dependent) {
			if(y < minimum) {
				minimum = y;
			}
			if(y > maximum) {
				maximum = y;
			}
		}
		double scale = Math.abs(100.0 / (maximum - minimum));
		List<String> newIndependent = new ArrayList<String>();
		Format decimalFormat = new DecimalFormat(".####");
		if(minimum < 0) {
			double offset = Math.abs(minimum);
			for (Double y : dependent) {
				newIndependent.add(decimalFormat.format((y + offset) * scale));
			}
		}
		else {
			for (Double y : dependent) {
				newIndependent.add(decimalFormat.format(y * scale));
			}
		}		
		return "-1|" + StringUtils.join(newIndependent, ",");
	}

	@Override
	public ModelInformation getInformation() {
		ModelInformation modelInformation = new ModelInformation();
		modelInformation.setName("Sine Wave").setLanguage("Java").setVersion("1.0");
		modelInformation.setDescription("Sine wave model with parameterized amplitude, frequency, and phase.");
		modelInformation.setProvider(this.getClass().getName());
		
		modelInformation.getEquations().add("y(x) = A \\cdot \\sin(\\omega x + \\phi)");

		modelInformation.getMetadata().add(new ModelMeta().setProperty("x0").setValue(Double.toString(getX0())).setDescription("Starting x value"));
		modelInformation.getMetadata().add(new ModelMeta().setProperty("x1").setValue(Double.toString(getX1())).setDescription("Ending x value"));
		modelInformation.getMetadata().add(new ModelMeta().setProperty("steps").setValue(Integer.toString(getSteps())).setDescription("Discretization steps"));
		
		modelInformation.getParameters().add(new ModelMeta().setProperty("amplitude").setValue(Double.toString(getAmplitude())).setDescription("Amplitude (A)"));
		modelInformation.getParameters().add(new ModelMeta().setProperty("frequency").setValue(Double.toString(getFrequency())).setDescription("Frequency (ω)"));
		modelInformation.getParameters().add(new ModelMeta().setProperty("phase").setValue(Double.toString(getPhase())).setDescription("Phase (ϕ)"));
		return modelInformation;
	}

	/**
	 * @return the x0
	 */
	public double getX0() {
		return x0;
	}

	/**
	 * @param x0 the x0 to set
	 */
	public void setX0(double x0) {
		this.x0 = x0;
	}

	/**
	 * @return the x1
	 */
	public double getX1() {
		return x1;
	}

	/**
	 * @param x1 the x1 to set
	 */
	public void setX1(double x1) {
		this.x1 = x1;
	}

	/**
	 * @return the steps
	 */
	public int getSteps() {
		return steps;
	}

	/**
	 * @param steps the steps to set
	 */
	public void setSteps(int steps) {
		this.steps = steps;
	}

	/**
	 * @return the amplitude
	 */
	public double getAmplitude() {
		return amplitude;
	}

	/**
	 * @param amplitude the amplitude to set
	 */
	public void setAmplitude(double amplitude) {
		this.amplitude = amplitude;
	}

	/**
	 * @return the frequency
	 */
	public double getFrequency() {
		return frequency;
	}

	/**
	 * @param frequency the frequency to set
	 */
	public void setFrequency(double frequency) {
		this.frequency = frequency;
	}

	/**
	 * @return the phase
	 */
	public double getPhase() {
		return phase;
	}

	/**
	 * @param phase the phase to set
	 */
	public void setPhase(double phase) {
		this.phase = phase;
	}
}
